查看原文
其他

工具&方法丨菜鸟升级打怪系列之python代码优化(2)

咪咪 数据Seminar 2021-06-03

前言

老角色咪咪又回来啦!不知道上一篇文介绍的os.walk遍历文件夹的方法算不算实用(实用的...吧...),所以这次俺又带来了使用python快速进行表格内或者表格间大数据量运算的方法。
设想一个场景,你手上有一张表,表上有一些字段以及数据,需要进行一些运算。如果数据量不大的话,你可能马上想到excel,只要选定单元格填写公式,再下拉填充就可以,人人都会使用excel进行列与列或者是行与行之间的运算!方法看似好简单,就是指定不同行的相同字段的数据进行同一个运算嘛,下拉填充不过只是快捷操作而已。所以直到手上的一个活儿也要求对表中的某几个字段使用某函数进行处理的时候,我也就自然而然地和excel中的情景联系在一起了,大不了在python中也一行一行的进行运算,用个 for 循环重复一下就好了!
天真如我,在一个需要运算将近300万行数据的任务面前,第一个就想到了这种方法,洋洋洒洒百余行代码,使用了4个进程,整整一天多才运行完毕(晕)。方法千千万万种,其实我也心里明白我这方法绝对不是效率最高的,所以才有了这篇代码优化文嘛!


正文


初方案--使用for循环实现
我使用for循环实现列运算的主要代码以及数据结构如下,以一个很简单的列之间的数学计算为例,逻辑比较简单易懂,就是使用for循环遍历DataFrame中的每一行,转化成可以传入calculate形式,并进行下文的计算。
数据结构如下,共有x、y、z三个字段,共33333 条数据。
import numpy as npimport pandas as pdimport math
# 随机生成33333行3列DataFramearr = np.random.choice(99999, size=99999).reshape(-1, 3) num_df = pd.DataFrame(arr, columns=['x', 'y', 'z'])

左右滑动查看更多

对上述三列数据进行数据计算,代码如下:
def calculate(x,y,z): """    三个数字的简单数学计算 """ res = math.sqrt(x) + math.sqrt(y) - math.sqrt(z) return res
num_df1 = num_df.copy()for j in num_df.index: # 按照DataFrame的索引取数据 xyz_num = num_df.loc[j, ['x', 'y', 'z']] # 取出需要计算的数据 res = calculate(xyz_num['x'], xyz_num['y'], xyz_num['z']) num_df1.loc[j, 'res'] = res
左右滑动查看更多
按照一般的思维(悄悄看见俺们公司实习生同学的代码也是用的for循环重复运算,强行两个人即为“一般”),运行下来是完全没有问题的(认真脸)。
但是!不出意外,又被技术部某位不愿意透露姓名的小刘老师泼冷水了。这涉及到工作效率问题,他隆重地给我介绍了 apply 方法,乍一听我还不能完全理解。后来发现,其实apply方法也还是属于对表格一行一行或者一列一列地进行运算,“apply”就是把函数运用到整个Series或是DataFrame中的意思。


代码优化--使用apply实现
Series和DataFrame都有apply方法,它们大同小异,为了便于理解,还是先来简单的看一下apply方法的使用方法和输出结果都是些啥。
首先无论是pandas.Series.apply还是pandas.DataFrame.apply都需要传入一个function,可以是def定义的普通函数也可以是lambda匿名函数。另外,pandas.DataFrame.apply有一个重要的可选参数axis,可以选择是按列取数还是按行取数。
# pandas.Series.apply示例res = num_df.loc[:,'z'].apply(lambda z_num: z_num + 1) # 对每个index的数据加1print(type(res)) # <class 'pandas.core.series.Series'>

左右滑动查看更多

# pandas.DataFrame.apply示例1res1 = num_df.apply(lambda num: num+1, axis=1)print(type(res1))  # <class 'pandas.core.frame.DataFrame'>

左右滑动查看更多

# pandas.DataFrame.apply示例2res2 = num_df.apply(lambda col:sum(col), axis=0)print(type(res2)) # <class 'pandas.core.series.Series'>

左右滑动查看更多

# pandas.DataFrame.apply示例3res3 = num_df.apply(lambda col:sum(col), axis=1)

左右滑动查看更多

根据pandas.DataFrame.apply示例1以及示例2发现,返回结果取决于apply内部function的运算。
示例2和示例3的不同点就是axis参数的取值不同,这里涉及到轴的概念,轴用来为超过一维的数组定义的属性,二维数据拥有两个轴:第0轴沿着行的垂直往下,第1轴沿着列的方向水平延伸。按照我的理解,axis=1是行操作,即同一行元素进行运算;axis=0是列操作,同一列内的元素进行运算。

了解完apply的基本知识点,终于可以回到了我们最开始的例题了!
这个时候就要用到DataFrame的apply方法,calculate函数需要传入三个变量,所以令axis=1进行行操作。运算过程大概类似于用DataFrame.loc方法取出一行的操作,可以理解成取出的是index为“x”、“y”、“z”的Series,所以在给calculate传入变量的时候的取值方法与Series的切片方法是一致的。
num_df1['res'] = num_df.apply(lambda ser: calculate(ser['x'], ser['y'], ser['z']),axis=1)
左右滑动查看更多
一行代码就完成!上面代码不仅简洁明了,而且效率也更高!


方法PK——哪种方法最高效?
为了更直观地看出方法间的效率差别,下面分别用for循环、DataFrame内部函数、apply方法三种方法来对行元素进行求和操作,通过运行时间的长短来看代码效率。
res = pd.Series([])# 1.使用for循环求和start_time = time.time()for i in num_df.index: res.loc[i] = sum(num_df.loc[i,:])print(res)print(time.time()-start_time) # 运行时间为:33.4246723651886s
# 2.使用DataFrame的内部函数DataFrame.sumstart_time = time.time()print(num_df.sum(axis=1))print(time.time()-start_time) # 运行时间为:0.003989696502685547s
# 3.使用applystart_time = time.time()print(num_df.apply(lambda col:sum(col), axis=1))print(time.time()-start_time)  # 运行时间为:0.38198184967041016s

左右滑动查看更多

总的来说:

for循环无疑是效率最低的;

当对象有相应的内部函数时尽量使用内部函数;

apply方法适用于较复杂算法的情景。

关于apply的更多参数,可以去查看一下官方文档👇

Series.apply(func, convert_dtype=True, args=(), **kwds)

Invoke function on values of Series.

Can be ufunc (a NumPy function that applies to the entire Series) or a Python function that only works on single values.

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.apply.html#pandas.Series.apply


DataFrame.apply(self, func, axis=0, broadcast=None, raw=False, reduce=None, result_type=None, args=(), **kwds)

Apply a function along an axis of the DataFrame.

Objects passed to the function are Series objects whose index is either the DataFrame’s index (axis=0) or the DataFrame’s columns (axis=1). By default (result_type=None), the final return type is inferred from the return type of the applied function. Otherwise, it depends on the result_type argument.

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.apply.html



apply方法加餐:DataFrame.groupby.apply
看完这些,是不是对apply的用法有些感觉了!那俺再顺着简单说一个也经常用到的DataFrame.groupby.apply方法。
首先先建立一个有重复值的表格,选择一列进行分类,再在每一个分组里进行相应地处理。
df = pd.DataFrame({'A':['a','b','a','c'], 'B':['c','d','d','a'], 'C':['x','x','f','b']})print(df.groupby(['A'])) # <pandas.core.groupby.groupby.DataFrameGroupBy object at 0x000000B0137ED6A0>

左右滑动查看更多

欣喜若狂地敲入代码,发现返回的是一个DataFrame的Groupby对象,类似于os.walk的返回结果,是一个二元组,包括组名和组成员,所以可以用for循环遍历。
for name, group in df.groupby(['A']): print(name) # 遍历该分组内的组名 print(group) # 遍历该组内的组成员 print(type(group), '\n')

左右滑动查看更多

a A B C0 a c x2 a d f<class 'pandas.core.frame.DataFrame'>
b A B C1 b d x<class 'pandas.core.frame.DataFrame'>
c A B C3 c a b<class 'pandas.core.frame.DataFrame'>

左右滑动查看更多

可以看到,按照“A”列分出的三个组被分别存放为DataFrame的形式。这就有点突然对DataFrame.groupby.apply方法有了更深一些的认知,我猜想是“依次取出每一个类别下的DataFrame进行操作”。

遂:

# DataFrame.to_dict方法将DataFrame转化成字典dict0 = df.groupby(['A']).apply(lambda df:df.reset_index(drop=True).to_dict('index')) print(dict0)

左右滑动查看更多

Aa {0: {'A': 'a', 'B': 'c', 'C': 'x'}, 1: {'A': '...b {0: {'A': 'b', 'B': 'd', 'C': 'x'}}c {0: {'A': 'c', 'B': 'a', 'C': 'b'}}dtype: object

左右滑动查看更多

果不其然!每个类别下的DataFrame都变成了字典并存放在了索引为他们的组名的Series里。


结语

python表格处理的三个(大概是?)常用对象的apply方法就是这些啦。虽然只是用了很简单的例子来举例,但是以小见大,肯定能够有用得上的时候!除此之外pandas库的方法还有很多,我也还在熟悉阶段,相信多学习多接触就可以继续开辟新大陆!


►往期推荐

回复【Python】👉 简单有用易上手


回复【学术前沿】👉机器学习丨大数据

回复【数据资源】👉公开数据

回复【可视化】👉 你心心念念的数据呈现

回复【老姚专栏】👉老姚趣谈值得一


►一周热文

工具&方法 | 6张卡片,2分钟,轻松掌握R命令大集合(推荐收藏备用)

特别推荐丨老姚专栏:理解工具变量的工具——需求定律

工具&方法丨经生小白会敲代码,还会写爬虫防坑指南



数据Seminar

这里是大数据、分析技术与学术研究的三叉路口



作者:咪咪(陈静晗)审阅:Dyson(刘颖波)编辑:青酱



    欢迎扫描👇二维码添加关注    


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存